#include "MultiLib.h"

const int CUBE_POINT_AMOUNT = 8;
const POINT WND_SIZE = { 1600, 900 };
const int THE_ANSWER = 42424242;

double GLOBAL_MATRIX [4] [4] =
{ { 1, 0, 0, 0 },
  { 0, 1, 0, 0 },
  { 0, 0, 1, 0 },
  { 0, 0, 0, 1 } };

struct VOXEL_POS
{
    double x, y, z;
};

struct point_t
{
    double x, y;
};

VOXEL_POS Rotate = {};

point_t CalcVoxel (VOXEL_POS pos);
void SetVoxel (VOXEL_POS pos, COLORREF color, double** z_buffer);
void PrspctiveVoxel (VOXEL_POS* pos);

void LineDraw (VOXEL_POS pos_1, VOXEL_POS pos_2, COLORREF color, double** z_buffer);
void DrawPrspctiveLine (VOXEL_POS pos_1, VOXEL_POS pos_2, COLORREF color, double** z_buffer);

void OutlineTriangle (VOXEL_POS pos [], double** z_buffer, COLORREF color);
void FillTriangle    (VOXEL_POS pos [], double** z_buffer, COLORREF color, int line_amount);
void DrawTriangle    (VOXEL_POS pos [], double** z_buffer, COLORREF clr, COLORREF outlinr_clr);

void DrawCube (VOXEL_POS a, double side_length, double** z_buffer);

void IK_rotateX (double a);
void IK_rotateY (double a);
void IK_rotateZ (double a);
void IK_rotate ();

void IK_moveX (double amount);
void IK_moveY (double amount);
void IK_moveZ (double amount);
void IK_move ();

void IK_stretchX (double amount);
void IK_stretchY (double amount);
void IK_stretchZ (double amount);
void IK_stretch ();

double dist_3D (VOXEL_POS a, VOXEL_POS b);
double dist_2D (point_t a, point_t b);
double Lerp (double a, double b, double k);
double cool_division (double num, double div);
void SortDots (VOXEL_POS pos [], double arr_size);
void swp (VOXEL_POS arr [], int pos_1, int pos_2);

int main ()
{
    double** z_buffer = new double* [WND_SIZE.x];
    for (int x = 0; x < WND_SIZE.x; x ++)
    {
        z_buffer [x] = new double [WND_SIZE.y];
    }

    txCreateWindow (WND_SIZE.x, WND_SIZE.y);
    VOXEL_POS pos = { 0, 0, 0 };
    int side_size = 100;

    txBegin ();

    while (!GetAsyncKeyState (VK_ESCAPE))
    {
        txSetFillColor (TX_BLACK);
        txClear ();

        DrawCube (pos, side_size, z_buffer);

        for (int x = 0; x < WND_SIZE.x; x ++)
        {
            for (int y = 0; y < WND_SIZE.y; y ++)
            {
                z_buffer [x] [y] = THE_ANSWER;
            }
        }

        txSleep (10);
    }

    txEnd ();

    for (int x = 0; x < WND_SIZE.x; x ++)
    {
        delete z_buffer [x];
    }

    delete z_buffer;

    return 0;
}

point_t CalcVoxel (VOXEL_POS pos)
{
    point_t pos_2d = {};

    pos_2d = { txGetExtentX ()/2 + pos.x/(-pos.z + 400) * 400,
               txGetExtentY ()/2 - pos.y/(-pos.z + 400) * 400 };

    return pos_2d;
}

void SetVoxel (VOXEL_POS pos, COLORREF color, double** z_buffer)
{
    point_t pos_2d = CalcVoxel (pos);
    if (In ((int) pos_2d.x, 0, (int) WND_SIZE.x) &&
        In ((int) pos_2d.y, 0, (int) WND_SIZE.y))
    {
        if (pos.z >= z_buffer [(int) pos_2d.x] [(int) pos_2d.y] ||
            z_buffer [(int) pos_2d.x] [(int) pos_2d.y] == THE_ANSWER)
        {
            txSetPixel (pos_2d.x, pos_2d.y, color);
            z_buffer [(int) pos_2d.x] [(int) pos_2d.y] = pos.z;
        }
    }
}

void LineDraw (VOXEL_POS pos_1, VOXEL_POS pos_2, COLORREF color, double** z_buffer)
{
    txSetColor (color);

    point_t pos_1_2D = CalcVoxel (pos_1),
            pos_2_2D = CalcVoxel (pos_2);

    double distance = dist_2D (pos_1_2D, pos_2_2D);
    if (distance > 0)
    {
        for (double i = 0; i < 1; i += 1/distance)
        {
            SetVoxel ({ Lerp (pos_1.x, pos_2.x, i),
                        Lerp (pos_1.y, pos_2.y, i),
                        Lerp (pos_1.z, pos_2.z, i) }, color, z_buffer);
        }
    }
}

void PrspctiveVoxel (VOXEL_POS* pos)
{
    double pos_vect [4] = { pos->x, pos->y, pos->z, 1 },
           data_storage [4] = {};

    IK_rotate ();
    IK_move ();
    IK_stretch ();

    mat_vec (GLOBAL_MATRIX, pos_vect, data_storage);

    pos->x = data_storage [0]/data_storage [3];
    pos->y = data_storage [1]/data_storage [3];
    pos->z = data_storage [2]/data_storage [3];
}

void DrawPrspctiveLine (VOXEL_POS pos_1, VOXEL_POS pos_2, COLORREF color, double** z_buffer)
{
    PrspctiveVoxel (&pos_1);
    PrspctiveVoxel (&pos_2);

    LineDraw (pos_1, pos_2, color, z_buffer);
}

void DrawCube (VOXEL_POS mid, double side_length, double** z_buffer)
{
    VOXEL_POS a = { mid.x - side_length/2, mid.y + side_length/2, mid.z + side_length/2 },
              b = { mid.x + side_length/2, mid.y - side_length/2, mid.z - side_length/2 };

    VOXEL_POS* curnt_trgl_pos = new VOXEL_POS [3];

    //{Top
    curnt_trgl_pos [0] = { a.x, a.y, b.z };
    curnt_trgl_pos [1] = { a.x, a.y, a.z };
    curnt_trgl_pos [2] = { b.x, a.y, a.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_RED, TX_WHITE);


    curnt_trgl_pos [0] = { b.x, a.y, a.z };
    curnt_trgl_pos [1] = { b.x, a.y, b.z };
    curnt_trgl_pos [2] = { a.x, a.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_YELLOW, TX_WHITE);

    //}

    //{Front
    curnt_trgl_pos [0] = { a.x, a.y, a.z };
    curnt_trgl_pos [1] = { a.x, b.y, a.z };
    curnt_trgl_pos [2] = { b.x, b.y, a.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_GREEN, TX_WHITE);


    curnt_trgl_pos [0] = { a.x, a.y, a.z };
    curnt_trgl_pos [1] = { b.x, a.y, a.z };
    curnt_trgl_pos [2] = { b.x, b.y, a.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_BLUE, TX_WHITE);
    //}

    //{Left
    curnt_trgl_pos [0] =  { a.x, b.y, a.z };
    curnt_trgl_pos [1] =  { a.x, a.y, a.z };
    curnt_trgl_pos [2] =  { a.x, a.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_PINK, TX_WHITE);


    curnt_trgl_pos [0] =  { a.x, a.y, b.z };
    curnt_trgl_pos [1] =  { a.x, b.y, b.z };
    curnt_trgl_pos [2] =  { a.x, b.y, a.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_BROWN, TX_WHITE);
    //}

    //{Right
    curnt_trgl_pos [0] =  { b.x, a.y, a.z };
    curnt_trgl_pos [1] =  { b.x, a.y, b.z };
    curnt_trgl_pos [2] =  { b.x, b.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_LIGHTBLUE, TX_WHITE);


    curnt_trgl_pos [0] =  { b.x, a.y, a.z };
    curnt_trgl_pos [1] =  { b.x, b.y, a.z };
    curnt_trgl_pos [2] =  { b.x, b.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_ORANGE, TX_WHITE);
    //}

    //{Back
    curnt_trgl_pos [0] =  { a.x, a.y, b.z };
    curnt_trgl_pos [1] =  { b.x, a.y, b.z };
    curnt_trgl_pos [2] =  { b.x, b.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_MAGENTA, TX_WHITE);


    curnt_trgl_pos [0] =  { a.x, a.y, b.z };
    curnt_trgl_pos [1] =  { a.x, b.y, b.z };
    curnt_trgl_pos [2] =  { b.x, b.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_CYAN, TX_WHITE);
    //}

    //{Bottom
    curnt_trgl_pos [0] =  { a.x, b.y, a.z };
    curnt_trgl_pos [1] =  { a.x, b.y, b.z };
    curnt_trgl_pos [2] =  { b.x, b.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_GRAY, TX_WHITE);


    curnt_trgl_pos [0] =  { a.x, b.y, a.z };
    curnt_trgl_pos [1] =  { b.x, b.y, a.z };
    curnt_trgl_pos [2] =  { b.x, b.y, b.z };

    DrawTriangle (curnt_trgl_pos, z_buffer, TX_LIGHTRED, TX_WHITE);
    //}

    delete curnt_trgl_pos;
}

void OutlineTriangle (VOXEL_POS pos [], double** z_buffer, COLORREF color)
{
    DrawPrspctiveLine (pos [0], pos [1], color, z_buffer);
    DrawPrspctiveLine (pos [1], pos [2], color, z_buffer);
    DrawPrspctiveLine (pos [2], pos [0], color, z_buffer);
}

void FillTriangle (VOXEL_POS pos [], double** z_buffer, COLORREF color, int line_amount)
{
    SortDots (pos, 3);

    double point4_lerp = cool_division (fabs (pos [2].y - pos [1].y), fabs (pos [2].y - pos [0].y));

    VOXEL_POS point_4 = { Lerp (pos [0].x, pos [2].x, point4_lerp),
                          Lerp (pos [0].y, pos [2].y, point4_lerp),
                          Lerp (pos [0].z, pos [2].z, point4_lerp) };


    line_amount ++;

    for (double k = 0; k < 1; k += (double) 1/line_amount)
    {
        DrawPrspctiveLine ( { Lerp (pos [2].x, point_4.x, k), Lerp (pos [2].y, point_4.y, k), Lerp (pos [2].z, point_4.z, k) },
                            { Lerp (pos [2].x, pos [1].x, k), Lerp (pos [2].y, pos [1].y, k), Lerp (pos [2].z, pos [1].z, k) }, color, z_buffer );
    }

    for (double k = 0; k < 1; k += (double) 1/line_amount)
    {
        DrawPrspctiveLine ( { Lerp (point_4.x, pos [0].x, k), Lerp (point_4.y, pos [0].y, k), Lerp (point_4.z, pos [0].z, k) },
                            { Lerp (pos [1].x, pos [0].x, k), Lerp (pos [1].x, pos [0].y, k), Lerp (pos [1].z, pos [0].z, k) }, color, z_buffer );

    }
}

void DrawTriangle (VOXEL_POS pos [], double** z_buffer, COLORREF clr, COLORREF outline_clr)
{
    OutlineTriangle (pos, z_buffer, outline_clr);
    FillTriangle (pos, z_buffer, clr, 200);
}

void IK_rotateX (double a)
{
    double x_rotation_matrix [4] [4] =
    { { 1, 0,       0,        0 },
      { 0, cos (a), -sin (a), 0 },
      { 0, sin (a),  cos (a), 0 },
      { 0, 0,       0,        1 } };

    double input_matrix [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (input_matrix, x_rotation_matrix, GLOBAL_MATRIX);
}

void IK_rotateY (double a)
{
    double y_rotation_matrix [4] [4]=
    { { cos (a), 0, -sin(a), 0 },
      { 0,       1,       0, 0 },
      { sin (a), 0, cos (a), 0 },
      { 0,       0,       0, 1 } };

    double input_matrix [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (y_rotation_matrix, input_matrix, GLOBAL_MATRIX);
}

void IK_rotateZ (double a)
{
    double z_rotation_matrix [4] [4] =
    { { cos (a), -sin (a), 0, 0 },
      { sin (a),  cos (a), 0, 0 },
      { 0,       0,        1, 0 },
      { 0,       0,        0, 1 } };

      double input_matrix [4] [4] = {};
      mat_cpy (GLOBAL_MATRIX, input_matrix);

      mat_mat (input_matrix, z_rotation_matrix, GLOBAL_MATRIX);
}

void IK_rotate ()
{
    const double vel = -0.0001;

    if (GetAsyncKeyState ('W')) IK_rotateX  (vel);
    if (GetAsyncKeyState ('S')) IK_rotateX (-vel);

    if (GetAsyncKeyState ('E')) IK_rotateZ  (vel);
    if (GetAsyncKeyState ('Q')) IK_rotateZ (-vel);

    if (GetAsyncKeyState ('A')) IK_rotateY (-vel);
    if (GetAsyncKeyState ('D')) IK_rotateY  (vel);
}

void IK_moveX (double amount)
{
    double x_move_matrix [4] [4] =
    { { 1, 0, 0, amount },
      { 0, 1, 0, 0      },
      { 0, 0, 1, 0      },
      { 0, 0, 0, 1      } };

    double input_matrix  [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (x_move_matrix, input_matrix, GLOBAL_MATRIX);
}

void IK_moveY (double amount)
{
    double y_move_matrix [4] [4] =
    { { 1, 0, 0, 0      },
      { 0, 1, 0, amount },
      { 0, 0, 1, 0      },
      { 0, 0, 0, 1      } };

    double input_matrix [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (input_matrix, y_move_matrix, GLOBAL_MATRIX);
}

void IK_moveZ (double amount)
{
    double z_move_matrix [4] [4] =
    { { 1, 0, 0, 0      },
      { 0, 1, 0, 0      },
      { 0, 0, 1, amount },
      { 0, 0, 0, 1      } };

    double input_matrix [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (input_matrix, z_move_matrix, GLOBAL_MATRIX);
}

void IK_move ()
{
    const double vel = -0.01;

    if (GetAsyncKeyState (VK_LEFT))    IK_moveX  (vel);
    if (GetAsyncKeyState (VK_RIGHT))   IK_moveX (-vel);

    if (GetAsyncKeyState (VK_DOWN))    IK_moveY (vel);
    if (GetAsyncKeyState (VK_UP))      IK_moveY (-vel);

    if (GetAsyncKeyState (VK_SHIFT))   IK_moveZ (vel);
    if (GetAsyncKeyState (VK_CONTROL)) IK_moveZ (-vel);
}

void IK_stretchX (double amount)
{
    double x_stretch_matrix [4] [4] =
    { { amount, 0, 0, 0 },
      { 0,      1, 0, 0 },
      { 0,      0, 1, 0 },
      { 0,      0, 0, 1 } };

    double input_matrix [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (input_matrix, x_stretch_matrix, GLOBAL_MATRIX);
}

void IK_stretchY (double amount)
{
    double y_stretch_matrix [4] [4] =
    { { 1, 0,      0, 0 },
      { 0, amount, 0, 0 },
      { 0, 0,      1, 0 },
      { 0, 0,      0, 1 } };

    double input_matrix [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (input_matrix, y_stretch_matrix, GLOBAL_MATRIX);
}

void IK_stretchZ (double amount)
{
    double z_stretch_matrix [4] [4] =
    { { 1, 0, 0,      0 },
      { 0, 1, 0,      0 },
      { 0, 0, amount, 0 },
      { 0, 0, 0,      1 } };

    double input_matrix [4] [4] = {};
    mat_cpy (GLOBAL_MATRIX, input_matrix);

    mat_mat (input_matrix, z_stretch_matrix, GLOBAL_MATRIX);
}

void IK_stretch ()
{
    const double bigger = 1.0001;
    const double smller = 0.9999;

    if (GetAsyncKeyState ('H')) IK_stretchX (bigger);
    if (GetAsyncKeyState ('F')) IK_stretchX (smller);

    if (GetAsyncKeyState ('T')) IK_stretchY (bigger);
    if (GetAsyncKeyState ('G')) IK_stretchY (smller);

    if (GetAsyncKeyState ('Y')) IK_stretchZ (bigger);
    if (GetAsyncKeyState ('R')) IK_stretchZ (smller);
}

double dist_3D (VOXEL_POS a, VOXEL_POS b)
{
    VOXEL_POS dist = { fabs (a.x - b.x), fabs (a.y - b.y), fabs (a.z - b.z) };

    return sqrt (dist.x * dist.x + dist.y * dist.y + dist.z * dist.z);
}

double dist_2D (point_t a, point_t b)
{
    point_t dist = { fabs (a.x - b.x), fabs (a.y - b.y) };

    return sqrt (dist.x * dist.x + dist.y * dist.y);
}

double Lerp (double a, double b, double k)
{
    double return_value = 0;

    assert (k >= 0 && k <= 1);
    return_value = a + k * (b-a);

    return return_value;
}

double cool_division (double num, double div)
{
    if (num == 0) return 0;
    return num/div;
}

void SortDots (VOXEL_POS pos [], double arr_size)
{
    for (bool free_pass = true; !free_pass; free_pass = true)
    {
        for (int i = 0; i + 1 < arr_size; i ++)
        {
            assert (i     >= 0 && i     < arr_size);
            assert (i + 1 >= 0 && i + 1 < arr_size);

            if (pos [i].y > pos [i + 1].y)
            {
                free_pass = false;
                swp (pos, i, i + 1);
            }
        }
    }
}

void swp (VOXEL_POS arr [], int pos_1, int pos_2)
{
    VOXEL_POS temp = arr [pos_1];
                     arr [pos_1] = arr [pos_2];
                                   arr [pos_2] = temp;
}
